package com.tuandai.log.log4j12.parser;

import com.tuandai.log.log4j12.converter.*;
import org.apache.log4j.helpers.Loader;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.pattern.*;

import java.lang.reflect.Method;
import java.util.*;

/**
 * Created by zhangzhenbin on 17-5-25.
 */
public class EnhancedPatternParserChain {
    /**
     * Escape character for format specifier.
     */
    private static final char ESCAPE_CHAR = '%';

    /**
     * Literal state.
     */
    private static final int LITERAL_STATE = 0;

    /**
     * In converter name state.
     */
    private static final int CONVERTER_STATE = 1;

    /**
     * Dot state.
     */
    private static final int DOT_STATE = 3;

    /**
     * Min state.
     */
    private static final int MIN_STATE = 4;

    /**
     * Max state.
     */
    private static final int MAX_STATE = 5;

    /**
     * Standard format specifiers for EnhancedPatternLayout.
     */
    private static final Map PATTERN_LAYOUT_RULES;

    /**
     * Standard format specifiers for rolling file appenders.
     */
    private static final Map FILENAME_PATTERN_RULES;

    static {
        // We set the global rules in the static initializer of PatternParser class
        Map rules = new HashMap(17);
        rules.put("c", LoggerPatternConverter.class);
        rules.put("logger", LoggerPatternConverter.class);

        rules.put("C", ClassNamePatternConverter.class);
        rules.put("class", ClassNamePatternConverter.class);

        rules.put("d", DatePatternConverter.class);
        rules.put("date", DatePatternConverter.class);

        //ZZB ADD
        //程序的Pid
        rules.put("P", ProcessIdConverter.class);
        rules.put("pid",ProcessIdConverter.class);

        //系统用户
        rules.put("U", SystemUserConverter.class);
        rules.put("sys_user",SystemUserConverter.class);

        //AppId
        rules.put("app_id",AppIdConverter.class);
        //去掉消息中的单双引号
        rules.put("encode_message", EncodedMessagePatternConverter.class);
        //当前主机Host名
        rules.put("host_name", HostNameConverter.class);

        rules.put("F", FileLocationPatternConverter.class);
        rules.put("file", FileLocationPatternConverter.class);

        rules.put("l", FullLocationPatternConverter.class);

        rules.put("L", LineLocationPatternConverter.class);
        rules.put("line", LineLocationPatternConverter.class);

        rules.put("m", MessagePatternConverter.class);
        rules.put("message", MessagePatternConverter.class);


        rules.put("n", LineSeparatorPatternConverter.class);

        rules.put("M", MethodLocationPatternConverter.class);
        rules.put("method", MethodLocationPatternConverter.class);

        rules.put("p", LevelPatternConverter.class);
        rules.put("level", LevelPatternConverter.class);

        rules.put("r", RelativeTimePatternConverter.class);
        rules.put("relative", RelativeTimePatternConverter.class);

        rules.put("t", ThreadPatternConverter.class);
        rules.put("thread", ThreadPatternConverter.class);

        rules.put("x", NDCPatternConverter.class);
        rules.put("ndc", NDCPatternConverter.class);

        rules.put("X", PropertiesPatternConverter.class);
        rules.put("properties", PropertiesPatternConverter.class);

        rules.put("sn", SequenceNumberPatternConverter.class);
        rules.put("sequenceNumber", SequenceNumberPatternConverter.class);

        rules.put("throwable", ThrowableInformationPatternConverter.class);
        PATTERN_LAYOUT_RULES = new EnhancedPatternParserChain.ReadOnlyMap(rules);

        Map fnameRules = new HashMap(4);
        fnameRules.put("d", FileDatePatternConverter.class);
        fnameRules.put("date", FileDatePatternConverter.class);
        fnameRules.put("i", IntegerPatternConverter.class);
        fnameRules.put("index", IntegerPatternConverter.class);


        FILENAME_PATTERN_RULES = new EnhancedPatternParserChain.ReadOnlyMap(fnameRules);
    }

    /**
     * Private constructor.
     */
    private EnhancedPatternParserChain() {
    }

    /**
     * Get standard format specifiers for EnhancedPatternLayout.
     * @return read-only map of format converter classes keyed by format specifier strings.
     */
    public static Map getPatternLayoutRules() {
        return PATTERN_LAYOUT_RULES;
    }

    /**
     * Get standard format specifiers for rolling file appender file specification.
     * @return read-only map of format converter classes keyed by format specifier strings.
     */
    public static Map getFileNamePatternRules() {
        return FILENAME_PATTERN_RULES;
    }

    /** Extract the converter identifier found at position i.
     *
     * After this function returns, the variable i will point to the
     * first char after the end of the converter identifier.
     *
     * If i points to a char which is not a character acceptable at the
     * start of a unicode identifier, the value null is returned.
     *
     * @param lastChar last processed character.
     * @param pattern format string.
     * @param i current index into pattern format.
     * @param convBuf buffer to receive conversion specifier.
     * @param currentLiteral literal to be output in case format specifier in unrecognized.
     * @return position in pattern after converter.
     */
    private static int extractConverter(
            char lastChar, final String pattern, int i, final StringBuffer convBuf,
            final StringBuffer currentLiteral) {
        convBuf.setLength(0);

        // When this method is called, lastChar points to the first character of the
        // conversion word. For example:
        // For "%hello"     lastChar = 'h'
        // For "%-5hello"   lastChar = 'h'
        //System.out.println("lastchar is "+lastChar);
        if (!Character.isUnicodeIdentifierStart(lastChar)) {
            return i;
        }

        convBuf.append(lastChar);

        while (
                (i < pattern.length())
                        && Character.isUnicodeIdentifierPart(pattern.charAt(i))) {
            convBuf.append(pattern.charAt(i));
            currentLiteral.append(pattern.charAt(i));

            //System.out.println("conv buffer is now ["+convBuf+"].");
            i++;
        }

        return i;
    }

    /**
     * Extract options.
     * @param pattern conversion pattern.
     * @param i start of options.
     * @param options array to receive extracted options
     * @return position in pattern after options.
     */
    private static int extractOptions(String pattern, int i, List options) {
        while ((i < pattern.length()) && (pattern.charAt(i) == '{')) {
            int end = pattern.indexOf('}', i);

            if (end == -1) {
                break;
            }

            String r = pattern.substring(i + 1, end);
            options.add(r);
            i = end + 1;
        }

        return i;
    }

    /**
     * Parse a format specifier.
     * @param pattern pattern to parse.
     * @param patternConverters list to receive pattern converters.
     * @param formattingInfos list to receive field specifiers corresponding to pattern converters.
     * @param converterRegistry map of user-supported pattern converters keyed by format specifier, may be null.
     * @param rules map of stock pattern converters keyed by format specifier.
     */
    public static void parse(
            final String pattern, final List patternConverters,
            final List formattingInfos, final Map converterRegistry, final Map rules) {
        if (pattern == null) {
            throw new NullPointerException("pattern");
        }

        StringBuffer currentLiteral = new StringBuffer(32);

        int patternLength = pattern.length();
        int state = LITERAL_STATE;
        char c;
        int i = 0;
        FormattingInfo formattingInfo = FormattingInfo.getDefault();

        while (i < patternLength) {
            c = pattern.charAt(i++);

            switch (state) {
                case LITERAL_STATE:

                    // In literal state, the last char is always a literal.
                    if (i == patternLength) {
                        currentLiteral.append(c);

                        continue;
                    }

                    if (c == ESCAPE_CHAR) {
                        // peek at the next char.
                        switch (pattern.charAt(i)) {
                            case ESCAPE_CHAR:
                                currentLiteral.append(c);
                                i++; // move pointer

                                break;

                            default:

                                if (currentLiteral.length() != 0) {
                                    patternConverters.add(
                                            new LiteralPatternConverter(currentLiteral.toString()));
                                    formattingInfos.add(FormattingInfo.getDefault());
                                }

                                currentLiteral.setLength(0);
                                currentLiteral.append(c); // append %
                                state = CONVERTER_STATE;
                                formattingInfo = FormattingInfo.getDefault();
                        }
                    } else {
                        currentLiteral.append(c);
                    }

                    break;

                case CONVERTER_STATE:
                    currentLiteral.append(c);

                    switch (c) {
                        case '-':
                            formattingInfo =
                                    new FormattingInfo(
                                            true, formattingInfo.getMinLength(),
                                            formattingInfo.getMaxLength());

                            break;

                        case '.':
                            state = DOT_STATE;

                            break;

                        default:

                            if ((c >= '0') && (c <= '9')) {
                                formattingInfo =
                                        new FormattingInfo(
                                                formattingInfo.isLeftAligned(), c - '0',
                                                formattingInfo.getMaxLength());
                                state = MIN_STATE;
                            } else {
                                i = finalizeConverter(
                                        c, pattern, i, currentLiteral, formattingInfo,
                                        converterRegistry, rules, patternConverters, formattingInfos);

                                // Next pattern is assumed to be a literal.
                                state = LITERAL_STATE;
                                formattingInfo = FormattingInfo.getDefault();
                                currentLiteral.setLength(0);
                            }
                    } // switch

                    break;

                case MIN_STATE:
                    currentLiteral.append(c);

                    if ((c >= '0') && (c <= '9')) {
                        formattingInfo =
                                new FormattingInfo(
                                        formattingInfo.isLeftAligned(),
                                        (formattingInfo.getMinLength() * 10) + (c - '0'),
                                        formattingInfo.getMaxLength());
                    } else if (c == '.') {
                        state = DOT_STATE;
                    } else {
                        i = finalizeConverter(
                                c, pattern, i, currentLiteral, formattingInfo,
                                converterRegistry, rules, patternConverters, formattingInfos);
                        state = LITERAL_STATE;
                        formattingInfo = FormattingInfo.getDefault();
                        currentLiteral.setLength(0);
                    }

                    break;

                case DOT_STATE:
                    currentLiteral.append(c);

                    if ((c >= '0') && (c <= '9')) {
                        formattingInfo =
                                new FormattingInfo(
                                        formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
                                        c - '0');
                        state = MAX_STATE;
                    } else {
                        LogLog.error(
                                "Error occured in position " + i
                                        + ".\n Was expecting digit, instead got char \"" + c + "\".");

                        state = LITERAL_STATE;
                    }

                    break;

                case MAX_STATE:
                    currentLiteral.append(c);

                    if ((c >= '0') && (c <= '9')) {
                        formattingInfo =
                                new FormattingInfo(
                                        formattingInfo.isLeftAligned(), formattingInfo.getMinLength(),
                                        (formattingInfo.getMaxLength() * 10) + (c - '0'));
                    } else {
                        i = finalizeConverter(
                                c, pattern, i, currentLiteral, formattingInfo,
                                converterRegistry, rules, patternConverters, formattingInfos);
                        state = LITERAL_STATE;
                        formattingInfo = FormattingInfo.getDefault();
                        currentLiteral.setLength(0);
                    }

                    break;
            } // switch
        }

        // while
        if (currentLiteral.length() != 0) {
            patternConverters.add(
                    new LiteralPatternConverter(currentLiteral.toString()));
            formattingInfos.add(FormattingInfo.getDefault());
        }
    }

    /**
     * Creates a new PatternConverter.
     *
     *
     * @param converterId converterId.
     * @param currentLiteral literal to be used if converter is unrecognized or following converter
     *    if converterId contains extra characters.
     * @param converterRegistry map of user-supported pattern converters keyed by format specifier, may be null.
     * @param rules map of stock pattern converters keyed by format specifier.
     * @param options converter options.
     * @return  converter or null.
     */
    private static PatternConverter createConverter(
            final String converterId, final StringBuffer currentLiteral,
            final Map converterRegistry, final Map rules, final List options) {
        String converterName = converterId;
        Object converterObj = null;

        for (int i = converterId.length(); (i > 0) && (converterObj == null);
             i--) {
            converterName = converterName.substring(0, i);

            if (converterRegistry != null) {
                converterObj = converterRegistry.get(converterName);
            }

            if ((converterObj == null) && (rules != null)) {
                converterObj = rules.get(converterName);
            }
        }

        if (converterObj == null) {
            LogLog.error("Unrecognized format specifier [" + converterId + "]");

            return null;
        }

        Class converterClass = null;

        if (converterObj instanceof Class) {
            converterClass = (Class) converterObj;
        } else {
            if (converterObj instanceof String) {
                try {
                    converterClass = Loader.loadClass((String) converterObj);
                } catch (ClassNotFoundException ex) {
                    LogLog.warn(
                            "Class for conversion pattern %" + converterName + " not found",
                            ex);

                    return null;
                }
            } else {
                LogLog.warn(
                        "Bad map entry for conversion pattern %" +  converterName + ".");

                return null;
            }
        }

        try {
            Method factory =
                    converterClass.getMethod(
                            "newInstance",
                            new Class[] {
                                    Class.forName("[Ljava.lang.String;")
                            });
            String[] optionsArray = new String[options.size()];
            optionsArray = (String[]) options.toArray(optionsArray);

            Object newObj =
                    factory.invoke(null, new Object[] { optionsArray });

            if (newObj instanceof PatternConverter) {
                currentLiteral.delete(
                        0,
                        currentLiteral.length()
                                - (converterId.length() - converterName.length()));

                return (PatternConverter) newObj;
            } else {
                LogLog.warn(
                        "Class " + converterClass.getName()
                                + " does not extend PatternConverter.");
            }
        } catch (Exception ex) {
            LogLog.error("Error creating converter for " + converterId, ex);

            try {
                //
                //  try default constructor
                PatternConverter pc = (PatternConverter) converterClass.newInstance();
                currentLiteral.delete(
                        0,
                        currentLiteral.length()
                                - (converterId.length() - converterName.length()));

                return pc;
            } catch (Exception ex2) {
                LogLog.error("Error creating converter for " + converterId, ex2);
            }
        }

        return null;
    }

    /**
     * Processes a format specifier sequence.
     *
     * @param c initial character of format specifier.
     * @param pattern conversion pattern
     * @param i current position in conversion pattern.
     * @param currentLiteral current literal.
     * @param formattingInfo current field specifier.
     * @param converterRegistry map of user-provided pattern converters keyed by format specifier, may be null.
     * @param rules map of stock pattern converters keyed by format specifier.
     * @param patternConverters list to receive parsed pattern converter.
     * @param formattingInfos list to receive corresponding field specifier.
     * @return position after format specifier sequence.
     */
    private static int finalizeConverter(
            char c, String pattern, int i,
            final StringBuffer currentLiteral, final FormattingInfo formattingInfo,
            final Map converterRegistry, final Map rules, final List patternConverters,
            final List formattingInfos) {
        StringBuffer convBuf = new StringBuffer();
        i = extractConverter(c, pattern, i, convBuf, currentLiteral);

        String converterId = convBuf.toString();

        List options = new ArrayList();
        i = extractOptions(pattern, i, options);

        PatternConverter pc =
                createConverter(
                        converterId, currentLiteral, converterRegistry, rules, options);

        if (pc == null) {
            StringBuffer msg;

            if ((converterId == null) || (converterId.length() == 0)) {
                msg =
                        new StringBuffer("Empty conversion specifier starting at position ");
            } else {
                msg = new StringBuffer("Unrecognized conversion specifier [");
                msg.append(converterId);
                msg.append("] starting at position ");
            }

            msg.append(Integer.toString(i));
            msg.append(" in conversion pattern.");

            LogLog.error(msg.toString());

            patternConverters.add(
                    new LiteralPatternConverter(currentLiteral.toString()));
            formattingInfos.add(FormattingInfo.getDefault());
        } else {
            patternConverters.add(pc);
            formattingInfos.add(formattingInfo);

            if (currentLiteral.length() > 0) {
                patternConverters.add(
                        new LiteralPatternConverter(currentLiteral.toString()));
                formattingInfos.add(FormattingInfo.getDefault());
            }
        }

        currentLiteral.setLength(0);

        return i;
    }

    /**
     * The class wraps another Map but throws exceptions on any attempt to modify the map.
     */
    private static class ReadOnlyMap implements Map {
        /**
         * Wrapped map.
         */
        private final Map map;

        /**
         * Constructor
         * @param src source map.
         */
        public ReadOnlyMap(Map src) {
            map = src;
        }

        /**
         * {@inheritDoc}
         */
        public void clear() {
            throw new UnsupportedOperationException();
        }

        /**
         * {@inheritDoc}
         */
        public boolean containsKey(Object key) {
            return map.containsKey(key);
        }

        /**
         * {@inheritDoc}
         */
        public boolean containsValue(Object value) {
            return map.containsValue(value);
        }

        /**
         * {@inheritDoc}
         */
        public Set entrySet() {
            return map.entrySet();
        }

        /**
         * {@inheritDoc}
         */
        public Object get(Object key) {
            return map.get(key);
        }

        /**
         * {@inheritDoc}
         */
        public boolean isEmpty() {
            return map.isEmpty();
        }

        /**
         * {@inheritDoc}
         */
        public Set keySet() {
            return map.keySet();
        }

        /**
         * {@inheritDoc}
         */
        public Object put(Object key, Object value) {
            throw new UnsupportedOperationException();
        }

        /**
         * {@inheritDoc}
         */
        public void putAll(Map t) {
            throw new UnsupportedOperationException();
        }

        /**
         * {@inheritDoc}
         */
        public Object remove(Object key) {
            throw new UnsupportedOperationException();
        }

        /**
         * {@inheritDoc}
         */
        public int size() {
            return map.size();
        }

        /**
         * {@inheritDoc}
         */
        public Collection values() {
            return map.values();
        }
    }

}
